using System;
using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Collections.Generic;

//
// * Copyright (c) 2002-2009 "Neo Technology,"
// *     Network Engine for Objects in Lund AB [http://neotechnology.com]
// *
// * This file is part of Neo4j.
// * 
// * Neo4j is free software: you can redistribute it and/or modify
// * it under the terms of the GNU Affero General Public License as
// * published by the Free Software Foundation, either version 3 of the
// * License, or (at your option) any later version.
// * 
// * This program is distributed in the hope that it will be useful,
// * but WITHOUT ANY WARRANTY; without even the implied warranty of
// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// * GNU Affero General Public License for more details.
// * 
// * You should have received a copy of the GNU Affero General Public License
// * along with this program. If not, see <http://www.gnu.org/licenses/>.
// 
namespace org.neo4j.kernel.impl.nioneo.store
{


///
/// <summary> * This class generates unique ids for a resource type. For example, nodes in a
/// * nodes space are connected to each other via relationships. On nodes and
/// * relationship one can add properties. We have three different resource types
/// * here (nodes, relationships and properties) where each resource needs a unique
/// * id to be able to differ resources of the same type from each other. Creating
/// * three id generators (one for each resource type ) will do the trick.
/// * <p>
/// * <CODE>IdGenerator</CODE> makes use of so called "defragged" ids. A
/// * defragged id is an id that has been in use one or many times but the resource
/// * that was using it doesn't exist anymore. This makes it possible to reuse the
/// * id and that in turn makes it possible to write a resource store with fixed
/// * records and size (you can calculate the position of a record by knowing the
/// * id without using indexes or a translation table).
/// * <p>
/// * The int value returned by the <seealso cref="#nextId"/> may not be the lowest you
/// * available id but will be one of the defragged ids if such exist or the next
/// * new free id that has never been used.
/// * <p>
/// * The <seealso cref="#freeId"/> will not check if the id passed in to it really is free.
/// * Passing a non free id will corrupt the id generator and <seealso cref="#nextId"/>
/// * method will eventually return that id.
/// * <p>
/// * The <seealso cref="#close()"/> method must always be invoked when done using an
/// * generator (for this time). Failure to do will render the generator as
/// * "sticky" and unusable next time you try to initialize a generator using the
/// * same file. Also you can only have one <CODE>IdGenerator</CODE> instance per
/// * id generator file at the same time.
/// * <p>
/// * In case of disk/file I/O failure an <CODE>IOException</CODE> is thrown. </summary>
/// 
	public class IdGeneratorImpl : IdGenerator
	{
	// sticky(byte), nextFreeId(long)
		private const int HEADER_SIZE = 9;

	// if sticky the id generator wasn't closed properly so it has to be
	// rebuilt (go through the node, relationship, property, rel type etc files)
		private const sbyte CLEAN_GENERATOR = (sbyte) 0;
		private const sbyte STICKY_GENERATOR = (sbyte) 1;

		private const long OVERFLOW_ID = 4294967294l;

	// number of defragged ids to grab form file in batch (also used for write)
		private int grabSize = -1;
		private long nextFreeId = -1;
	// total bytes read from file, used in writeIdBatch() and close()
		private long totalBytesRead = 0;
	// true if more defragged ids can be read from file
		private bool haveMore = true;
	// marks where this sessions released ids will be written
		private long readBlocksTo = HEADER_SIZE;
	// used to calculate number of ids actually in use
		private long defraggedIdCount = -1;

		private readonly string fileName;
		private FileChannel fileChannel = null;
	// in memory defragged ids read from file (and from freeId)
		private readonly LinkedList<long> defragedIdList = new LinkedList<long>();
	// in memory newly free defragged ids that havn't been flushed to disk yet
		private readonly LinkedList<long> releasedIdList = new LinkedList<long>();
	// buffer used in readIdBatch()
		private ByteBuffer readBuffer = null;
	// buffer used in writeIdBatch() and close()
		private ByteBuffer writeBuffer = null;

///    
///     <summary> * Opens the id generator represented by <CODE>fileName</CODE>. The
///     * <CODE>grabSize</CODE> means how many defragged ids we should keep in
///     * memory and is also the size (x4) of the two buffers used for reading and
///     * writing to the id generator file. The highest returned id will be read
///     * from file and if <CODE>grabSize</CODE> number of ids exist they will be
///     * read into memory (if less exist all defragged ids will be in memory).
///     * <p>
///     * If this id generator hasn't been closed properly since the previous
///     * session (sticky) an <CODE>IOException</CODE> will be thrown. When this
///     * happens one has to rebuild the id generator from the (node/rel/prop)
///     * store file.
///     *  </summary>
///     * <param name="fileName">
///     *            The file name (and path if needed) for the id generator to be
///     *            opened </param>
///     * <param name="grabSize">
///     *            The number of defragged ids to keep in memory </param>
///     * <exception cref="IOException">
///     *             If no such file exist or if the id generator is sticky </exception>
///     
		public IdGeneratorImpl(string fileName, int grabSize)
		{
			if (grabSize < 1)
			{
				throw new IllegalArgumentException("Illegal grabSize: " + grabSize);
			}
			this.fileName = fileName;
			this.grabSize = grabSize;
			readBuffer = ByteBuffer.allocate(grabSize * 8);
			writeBuffer = ByteBuffer.allocate(grabSize * 8);
			initGenerator();
		}

///    
///     <summary> * Returns the next "free" id. If a defragged id exist it will be returned
///     * else the next free id that hasn't been used yet is returned. If no id
///     * exist the capacity is exceeded (all int values >= 0 are taken) and a
///     * <CODE>IOException</CODE> will be thrown.
///     *  </summary>
///     * <returns> The next free id </returns>
///     * <exception cref="IOException">
///     *             If the capacity is exceeded or closed generator </exception>
///     
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual long nextId()
		{
			if (fileChannel == null)
			{
				throw new IllegalStateException("Closed id generator " + fileName);
			}
			if (defragedIdList.Count > 0)
			{
				long id = defragedIdList.RemoveFirst();
				if (haveMore && defragedIdList.Count == 0)
				{
					readIdBatch();
				}
				defraggedIdCount--;
				return id;
			}
			if (nextFreeId >= OVERFLOW_ID || nextFreeId < 0)
			{
				throw new UnderlyingStorageException("Id capacity exceeded");
			}
			return nextFreeId++;
		}

///    
///     <summary> * Sets the next free "high" id. This method should be called when an id
///     * generator has been rebuilt.
///     *  </summary>
///     * <param name="id">
///     *            The next free id </param>
///     
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void setHighId(long id)
		{
			nextFreeId = id;
		}

///    
///     <summary> * Returns the next "high" id that will be returned if no defragged ids
///     * exist.
///     *  </summary>
///     * <returns> The next free "high" id </returns>
///     
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual long getHighId()
		{
			return nextFreeId;
		}

///    
///     <summary> * Frees the <CODE>id</CODE> making it a defragged id that will be
///     * returned by next id before any new id (that hasn't been used yet) is
///     * returned.
///     * <p>
///     * This method will throw an <CODE>IOException</CODE> if id is negative or
///     * if id is greater than the highest returned id. However as stated in the
///     * class documentation above the id isn't validated to see if it really is
///     * free.
///     *  </summary>
///     * <param name="id">
///     *            The id to be made available again </param>
///     * <exception cref="IOException">
///     *             If id is negative or greater than the highest returned id </exception>
///     
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void freeId(long id)
		{
			if (id < 0 || id >= nextFreeId)
			{
				throw new IllegalArgumentException("Illegal id[" + id + "]");
			}
			if (fileChannel == null)
			{
				throw new IllegalStateException("Generator closed " + fileName);
			}
			releasedIdList.AddLast(id);
			defraggedIdCount++;
			if (releasedIdList.Count >= grabSize)
			{
				writeIdBatch();
			}
		}

///    
///     <summary> * Closes the id generator flushing defragged ids in memory to file. The
///     * file will be truncated to the minimal size required to hold all defragged
///     * ids and it will be marked as clean (not sticky).
///     * <p>
///     * An invoke to the <CODE>nextId</CODE> or <CODE>freeId</CODE> after
///     * this method has been invoked will result in an <CODE>IOException</CODE>
///     * since the highest returned id has been set to a negative value.
///     *  </summary>
///     * <exception cref="IOException">
///     *             If unable to close this id generator </exception>
///     
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void close()
		{
			if (nextFreeId == -1)
			{
				return;
			}

		// write out lists
			if (releasedIdList.Count > 0)
			{
				writeIdBatch();
			}
			if (defragedIdList.Count > 0)
			{
				while (defragedIdList.Count > 0)
				{
					releasedIdList.AddLast(defragedIdList.RemoveFirst());
				}
				writeIdBatch();
			}

		// write header
			try
			{
				fileChannel.position(0);
				ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE);
				buffer.put(STICKY_GENERATOR).putLong(nextFreeId);
				buffer.flip();
				fileChannel.write(buffer);
			// move data to remove fragmentation in file
				if (totalBytesRead > HEADER_SIZE)
				{
					long writePosition = HEADER_SIZE;
					long readPosition = readBlocksTo;
					if (totalBytesRead < readBlocksTo)
					{
						readPosition = totalBytesRead;
					}
					int bytesRead = -1;
					do
					{
						writeBuffer.Clear();
						fileChannel.position(readPosition);
						bytesRead = fileChannel.read(writeBuffer);
						readPosition += bytesRead;
						writeBuffer.flip();
						fileChannel.position(writePosition);
						writePosition += fileChannel.write(writeBuffer);
					}
					while (bytesRead > 0);
				// truncate
					fileChannel.truncate(writePosition);
				}
			// flush
				fileChannel.force(false);
			// remove sticky
				buffer.Clear();
				buffer.put(CLEAN_GENERATOR);
				buffer.limit(1);
				buffer.flip();
				fileChannel.position(0);
				fileChannel.write(buffer);
			// flush and close
				fileChannel.force(false);
				fileChannel.Close();
				fileChannel = null;
			// make this generator unusable
				nextFreeId = -1;
			}
			catch (IOException e)
			{
				throw new UnderlyingStorageException("Unable to close id generator " + fileName, e);
			}
		}

///    
///     <summary> * Returns the file associated with this id generator.
///     *  </summary>
///     * <returns> The id generator's file name </returns>
///     
		public virtual string getFileName()
		{
			return this.fileName;
		}

///    
///     <summary> * Creates a new id generator.
///     *  </summary>
///     * <param name="fileName">
///     *            The name of the id generator </param>
///     * <exception cref="IOException">
///     *             If unable to create the id generator </exception>
///     
		public static void createGenerator(string fileName)
		{
		// sanity checks
			if (fileName == null)
			{
				throw new IllegalArgumentException("Null filename");
			}
			File file = new File(fileName);
			if (file.exists())
			{
				throw new IllegalStateException("Can't create IdGeneratorFile[" + fileName + "], file already exists");
			}
			try
			{
				FileChannel channel = new FileOutputStream(fileName).getChannel();
			// write the header
				ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE);
				buffer.put(CLEAN_GENERATOR).putLong(0).flip();
				channel.write(buffer);
				channel.force(false);
				channel.Close();
			}
			catch (IOException e)
			{
				throw new UnderlyingStorageException("Unable to create id generator" + fileName, e);
			}
		}

	// initialize the id generator and performs a simple validation
		[MethodImpl(MethodImplOptions.Synchronized)]
		private void initGenerator()
		{
			try
			{
				fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
				ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE);
				totalBytesRead = fileChannel.read(buffer);
				if (totalBytesRead != HEADER_SIZE)
				{
					fileChannel.Close();
					throw new InvalidIdGeneratorException("Unable to read header, bytes read: " + totalBytesRead);
				}
				buffer.flip();
				sbyte storageStatus = buffer.get();
				if (storageStatus != CLEAN_GENERATOR)
				{
					fileChannel.Close();
					throw new InvalidIdGeneratorException("Sticky generator[ " + fileName + "] delete this id generator and build a new one");
				}
				this.nextFreeId = buffer.getLong();
				buffer.flip();
				buffer.put(STICKY_GENERATOR).limit(1).flip();
				fileChannel.position(0);
				fileChannel.write(buffer);
				fileChannel.position(HEADER_SIZE);
				readBlocksTo = fileChannel.size();
				defraggedIdCount = (int)(readBlocksTo - HEADER_SIZE) / 8;
				readIdBatch();
			}
			catch (IOException e)
			{
				throw new UnderlyingStorageException("Unable to init id generator " + fileName, e);
			}
		}

		private void readIdBatch()
		{
			if (!haveMore)
			{
				return;
			}
			if (totalBytesRead >= readBlocksTo)
			{
				haveMore = false;
				return;
			}
			try
			{
				if (totalBytesRead + readBuffer.capacity() > readBlocksTo)
				{
					readBuffer.Clear();
					readBuffer.limit((int)(readBlocksTo - fileChannel.position()));
				}
				else
				{
					readBuffer.Clear();
				}
				fileChannel.position(totalBytesRead);
				int bytesRead = fileChannel.read(readBuffer);
				Debug.Assert(fileChannel.position() <= readBlocksTo);
				totalBytesRead += bytesRead;
				readBuffer.flip();
				Debug.Assert((bytesRead % 8) == 0);
				int idsRead = bytesRead / 8;
				defraggedIdCount -= idsRead;
				for (int i = 0; i < idsRead; i++)
				{
					long id = readBuffer.getLong();
					defragedIdList.AddLast(id);
				}
			}
			catch (IOException e)
			{
				throw new UnderlyingStorageException("Failed reading defragged id batch", e);
			}
		}

	// writes a batch of defragged ids to file
		private void writeIdBatch()
		{
		// position at end
			try
			{
				fileChannel.position(fileChannel.size());
				writeBuffer.Clear();
				while (releasedIdList.Count > 0)
				{
					writeBuffer.putLong(releasedIdList.RemoveFirst());
					if (writeBuffer.position() == writeBuffer.capacity())
					{
						writeBuffer.flip();
						fileChannel.write(writeBuffer);
						writeBuffer.Clear();
					}
				}
				writeBuffer.flip();
				fileChannel.write(writeBuffer);
			// position for next readIdBatch
				fileChannel.position(totalBytesRead);
			}
			catch (IOException e)
			{
				throw new UnderlyingStorageException("Unable to write defragged id " + " batch", e);
			}
		}

///    
///     <summary> * Utility method that will dump all defragged id's and the "high id" to
///     * console. Do not call while running store using this id generator since it
///     * could corrupt the id generator (not thread safe). This method will close
///     * the id generator after being invoked.
///     *  </summary>
///     * <exception cref="IOException">
///     *             If problem dumping free ids </exception>
///     
		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual void dumpFreeIds()
		{
			while (haveMore)
			{
				readIdBatch();
			}
			LinkedList<long>.Enumerator itr = defragedIdList.GetEnumerator();
			while (itr.MoveNext())
			{
				Console.Write(" " + itr.Current);
			}
			Console.WriteLine("\nNext free id: " + nextFreeId);
			Close();
		}

		[MethodImpl(MethodImplOptions.Synchronized)]
		public virtual long getNumberOfIdsInUse()
		{
			return nextFreeId - defraggedIdCount;
		}
	}
}